package asteroids.model;

import java.util.*;

import be.kuleuven.cs.som.annotate.*;

import ModelExceptions.*;
import asteroids.CollisionListener;
import asteroids.Util;
import asteroids.model.programs.Program;

public class World {



	/**
	 * 
	 * @param height
	 * @param width
	 * @post	...
	 * 			|new.getHeight() == height
	 * @post	...
	 * 			|new.getWidth() == width;
	 * @throws OutOfBoundsException	...
	 * 								|!isValidWorldSize(height, width)
	 * @invar 	...
	 * 			|isValidWoldSize(getWorldHeight(), getWorldWidth())
	 * @invar	...
	 * 			| getLeftBoundary() < getRightBoundary()
	 * @invar 	...
	 * 			| getUpperBoundary() > getLowerBoundary()
	 * @invar	...
	 * 			| hasProperCollisionObjects()
	 * @invar	...
	 * 			| foreach Collisionobject collisionObject in getGameObjects()
	 * 			|		Util.isLocatedWithinRange(getLeftBoundary(),getRightBoundary(), getXCoordinate())
	 * 			|		&& isLocatedWithinRange(getUpperBoundary(),getLowerBoundary(), getYCoordinate())
	 */
	public World(double height, double width) throws OutOfBoundsException{
		if(!isValidWorldSize(height, width))
			throw new OutOfBoundsException("The given width and height are not applicable to create a world"); 
		this.width = width;
		this.height = height;

		gameObjects = new HashSet<CollisionObject>();
	}


	private final double maxHeight 	= 	Double.MAX_VALUE;
	private final double maxWidth 	=	Double.MAX_VALUE;
	private final double minHeight	=	0;
	private final double minWidth	=	0;
	private final double height;
	private final double width;
	private final Set<CollisionObject> gameObjects;




	/**
	 * Check whether a world with given height and width would be valid
	 * @param height
	 * @param width
	 * @return 	...
	 * 			|result == (isValidHeight(height) && isValidWidth(width))
	 */
	public boolean isValidWorldSize(double height, double width){
		return (isValidHeight(height) && isValidWidth(width));
	}


	/**
	 * Get the height of the world
	 * @return The height of the world
	 */
	@Basic @Immutable
	public double getWorldHeight(){
		return this.height;
	}

	@Basic @Immutable
	public double getMinHeight(){
		return this.minHeight;
	}

	@Basic @Immutable
	public double getMaxHeight(){
		return this.maxHeight;
	}

	/**
	 * 
	 * @param height
	 * @return	...
	 * 			|result == (Util.isLocatedWithinRange(getMinHeight(), getMaxHeight(), height)  && Double.isNaN(height))
	 */
	public boolean isValidHeight(double height){
		if(!Util.isLocatedWithinRange(getMinHeight(), getMaxHeight(), height) ||Double.isNaN(height)){
			return false;
		}
		return true;
	}	


	@Basic	@Immutable
	public double getWorldWidth(){
		return this.width;
	}

	@Basic @Immutable
	public double getMinWidth(){
		return this.minWidth;
	}

	@Basic @Immutable
	public double getMaxWidth(){
		return this.maxWidth;
	}

	/**
	 * 
	 * @param width 	The width of the world
	 * @return 			...
	 * 					| result == Util.isLocatedWithinRange(getMinWidth(), getMaxWidth(), width) && Double.isNaN(width)
	 */
	private boolean isValidWidth(double width){

		if(!Util.isLocatedWithinRange(getMinWidth(), getMaxWidth(), width) ||Double.isNaN(width)){
			return false;
		}
		return true;
	}



	/**
	 * 
	 * @return	...
	 * 			|result == 0
	 */
	public double getLeftBoundary(){
		return 0;
	}

	/**
	 * 
	 * @return	...
	 * 			|result == getWorldWidth();
	 */
	public double getRightBoundary(){
		return getWorldWidth();
	}

	/**
	 * 
	 * @return	...
	 * 			| result == 0
	 */
	public double getLowerBoundary(){
		return 0;
	}

	/**
	 * 
	 * @return	...
	 * 			| result == getWorldHeight
	 */
	public double getUpperBoundary(){
		return getWorldHeight();
	}

	/**
	 * 
	 * @param collisionObject	The Object you want to check
	 * @return					|if(collisionObject.isShip()||collisionObject.isAsteroid()||collisionObject.isBullet()) then
	 *							| result == true;
	 */
	@SuppressWarnings("unused")
	private boolean isValidObject(CollisionObject collisionObject){
		if(collisionObject.isShip()||collisionObject.isAsteroid()||collisionObject.isBullet()){
			return true;
		}
		else return false;
	}

	@Basic 
	public Set<CollisionObject> getGameObjects(){
		Set<CollisionObject> returnObjectSet = new HashSet<CollisionObject>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject colissionObject = it.next();
			returnObjectSet.add(colissionObject);
		}
		//The return is a clone
		return returnObjectSet;
	}

	/**
	 * 
	 * @return	...
	 * 			|result == getGameObjects().size()
	 */
	@Basic 
	public int getNbOfGameObjects(){
		return this.getGameObjects().size();
	}

	/**
	 * 
	 * @param collisionObject The object you want to check
	 * @return 	...
	 * 			|result == getGameObjects().contains(collisionObject);
	 * 
	 */
	public boolean hasAsCollisionObject(CollisionObject collisionObject){
		return getGameObjects().contains(collisionObject);
	}

	/**
	 * 
	 * @param collisionObject	The collisionObject you want to add 
	 * @post					...
	 * 							|if(canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this)) then
	 * 							|		hasAsCollisionObject(collisionObject) == true	
	 * @throws	IllegalCollisionObjectException	...
	 * 											|	!(canHaveAsCollisionObject(collisionObject) || !collisionObject.canHaveAsWorld(this))
	 */
	public void addAsCollisionObject(@Raw CollisionObject collisionObject) throws IllegalCollisionObjectException{
		if(canHaveAsCollisionObject(collisionObject) && collisionObject.isInNoWorld()){
			this.gameObjects.add(collisionObject);
			if(collisionObject.getWorld() != this)
				collisionObject.setWorld(this);
		}
		else{
			throw new IllegalCollisionObjectException("Unallowed add");
		}

	}

	/**
	 * @param collisionObject			The CollisionObject you want to remove from the world		
	 * @post							...
	 * 									|!new.hasAsCollisionObject(collisionObject) 
	 * @throws IllegalCollisionObjectException	...
	 * 											|!hasAsCollisionObject(collisionObject)
	 */
	public void removeAsCollisionObject(CollisionObject collisionObject) throws IllegalCollisionObjectException{
		if(hasAsCollisionObject(collisionObject)){
			this.gameObjects.remove(collisionObject);
			if(collisionObject.getWorld()!=null)
				collisionObject.setWorld(null);
		}
		else{
			throw new IllegalCollisionObjectException("The given collisionobject is not situated in this world");
		}
	}

	/**
	 * 
	 * @param collisionObject 	The CollissionObject you want to destroy
	 * @post					||collisionObject.destroy
	 * 							||this.hasAsCollisionObject(collisionObject) == false
	 */
	public void destroyCollisionObject(CollisionObject collisionObject){
		if(collisionObject.getWorld()==this){
			removeAsCollisionObject(collisionObject);
			collisionObject.destroy();
		}
		//else do nothing.
	}

	/**
	 * 
	 * @param collisionObject	The object you want to check
	 * @return					...
	 * 							|if (collisionObject!=null && !collisionObject.isDestroyed())
	 *							|result == true;
	 */
	public boolean canHaveAsCollisionObject(CollisionObject collisionObject){
		if (collisionObject!=null && !collisionObject.isDestroyed())
			return true;
		return false;
	}

	/**
	 * 
	 * @return	...
	 * 			| while(it.hasNext()) do{
	 * 			|		CollisionObject collisionObject = it.next();
	 * 			|		if(!this.canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this) && collisionObject.getWorld() != this)
	 * 			|			return false;
	 * 			| }
				|return true;
	 */
	public boolean hasProperCollisionObjects(){

		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			if(collisionObject.getWorld() != this && !this.canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this))
				return false;
		}
		return true;
	}


	/**
	 * Return all ships located within world.
	 * @return	...
	 * 			|Set<Ship> resultSet = new HashSet<Ship>();
	 * 			|for each collisionobject in this.getGameObjects():
	 * 			|	if(collisionObject.isShip());
	 *			|		resultSet.add((Ship) collisionObject);
	 *			|return resultSet;
	 */
	public Set<Ship> getShips(){
		Set<Ship> resultSet = new HashSet<Ship>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{
				if(collisionObject.isShip())
					resultSet.add((Ship) collisionObject);
			}
			catch (ClassCastException exc){

			}
		}
		return resultSet;

	}

	/**
	 * Return all asteroids located in world.
	 * @return	...
	 * 			|Set<Asteroid> resultSet = new HashSet<Asteroid>();
	 * 			|for each collisionobject in this.getGameObjects():
	 * 			|	if(collisionObject.isAsteroid());
	 *			|		resultSet.add((Asteroid) collisionObject);}
	 *			|return resultSet;
	 */
	public Set<Asteroid> getAsteroids(){
		Set<Asteroid> resultSet = new HashSet<Asteroid>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{
				if(collisionObject.isAsteroid())
					resultSet.add((Asteroid)collisionObject);
			}
			catch(ClassCastException exc){

			}
		}
		return resultSet;
	}

	/**
	 * Return all bullets located in world.
	 * @return	|Set<Bullet> resultSet = new HashSet<Bullet>();
	 * 			|for each collisionobject in this.getGameObjects():
	 * 			|	if(collisionObject.isBullet());
	 *			|		resultSet.add((Bullet) collisionObject);}
	 *			|return resultSet;
	 */
	public Set<Bullet> getBullets(){
		Set<Bullet> resultSet = new HashSet<Bullet>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{
				if(collisionObject.isBullet()){
					resultSet.add((Bullet)collisionObject);
				}
			}
			catch(ClassCastException exc){

			}
		}
		return resultSet;
	}





	/**
	 * 
	 * @return	...
	 * 			|CollisionObject firstCollisionObject 
	 * 			|CollisionObject firstCollisionObjectPartner
	 * 			|double xFirstCollisionObject
	 * 			|double yFirstCollisionObject
	 * 			|for each collisionobject in this.getGameObjects():
	 * 			|	for each collisionobjectpartner in this.getGameObjects():
	 * 			|		double collisionTime = collisionObject.getTimeToCollision(collisionObjectPartner);
	 * 			|		if(collisionTime < timeToFirstCollision && !Util.fuzzyLessThanOrEqualTo(collisionTime, 0)){
	 * 			|			timeToFirstCollision = collisionTime
	 * 			|			firstCollisionObject = collisionObject
	 * 			|			xFirstCollisionObject = firstCollisionObject.getXCoordinate()
	 * 			|			yFirstCollisionObject = firstCollisionObject.getYCoordinate()
	 * 			|			firstCollisionObjectPartner = collisionObjectPartner}}}
	 * 			|CollisionObject[] collisionPartners = {firstCollisionObject, firstCollisionObjectPartner};
	 *			|result == collisionPartners
	 */
	private CollisionObject[] getFirstCollision(){
		CollisionObject firstCollisionObject 		= null;
		CollisionObject firstCollisionObjectPartner	= null;
		double xFirstCollisionObject = 0;
		double yFirstCollisionObject = 0;
		//STEP1: FIRST COLLISION
		double timeToFirstCollision = Double.POSITIVE_INFINITY;
		for(CollisionObject collisionObject: getGameObjects()){
			for(CollisionObject collisionObjectPartner: getGameObjects()){
				if(collisionObject != null && collisionObject != collisionObjectPartner){	//impliciet getest of colisisionObjectPartner ook niet null is

					double collisionTime = collisionObject.getTimeToCollision(collisionObjectPartner);
					if(!Util.fuzzyEquals(timeToFirstCollision, Double.POSITIVE_INFINITY)&&Util.fuzzyEquals(collisionTime,timeToFirstCollision) && !Util.fuzzyEquals(xFirstCollisionObject,collisionObjectPartner.getXCoordinate())&& !Util.fuzzyEquals(yFirstCollisionObject,collisionObjectPartner.getYCoordinate())){
						collisionObjectPartner.setValidVelocity(collisionObjectPartner.getXVelocity() - 1, collisionObjectPartner.getYVelocity() - 1);
					}
					if(collisionTime < timeToFirstCollision && !Util.fuzzyLessThanOrEqualTo(collisionTime, 0)){
						timeToFirstCollision = collisionTime;
						firstCollisionObject = collisionObject;
						xFirstCollisionObject = firstCollisionObject.getXCoordinate();
						yFirstCollisionObject = firstCollisionObject.getYCoordinate();

						firstCollisionObjectPartner = collisionObjectPartner;
					}
				}
			}
		}
		CollisionObject[] collisionPartners = {firstCollisionObject, firstCollisionObjectPartner};
		return collisionPartners;
	}

	/**
	 * 
	 * @param dt							The time you want to forward
	 * @param collisionListener				The object that is used to visually represent collisions
	 * @throws IllegalTimeDurationException	If the time is less then or equal to zero or is infinty/nan
	 * 										|if(Util.fuzzyLessThanOrEqualTo(dt, 0)) || Double.isNaN(dt) || dt == Double.POSITIVE_INFINITY || dt== Double.NEGATIVE_INFINITY){
	 * 
	 * @post	...
	 * 			|if(!Util.fuzzyLessThanOrEqualTo(dt, 0)) || !Double.isNaN(dt) || !dt == Double.POSITIVE_INFINITY || !dt== Double.NEGATIVE_INFINITY){
	 *
	 * @effect	...
	 * 			| for each collisionobject in this.getGameObjects():
	 * 			|		if(!Util.fuzzyLessThanOrEqualTo(collisionobject.getTimeToCollision(),dt) then
	 * 			|			moveGameObjects(dt);
	 * @effect	...
	 * 			| 	boolean finished = false;
	 *			|	CollisionObject[] first= getFirstCollision();
	 *			|	firstCollisionObject = first[0];
	 *			|	firstCollisionObjectPartner = first[1];
	 *			|	double timeToFirstCollision = firstCollisionObject.getTimeToCollision(firstCollisionObjectPartner);
	 * 			|	while(!finished){
	 * 			|	for each collsionobject in this.getGameObjects():
	 * 			|		if(Util.fuzzyLessThanOrEqualTo(collisionTime,dt) then
	 * 			|			collisionobject.move(collisionTime);
	 * 			|			collisionobject.collisionreactionwith(collisionPartner);
	 * 			|			dt	= dt-collisionTime;
	 * 			|			
	 * 			|			finished = true;
	 * 			
	 * @effect	...
	 * 			|if(dt<getTimeForProgramExec()) then
	 * 			| 	setProgramUnexecutedTime(dt)
	 * 
	 * @effect 	...
	 * 			|if(getProgramUnexecutedTime()>=getTimeForProgramExec()) then
	 *			|	execAllPrograms();
	 *			|	resetProgramUnexecutedTime();
	 *
	 * @post	...
	 *			|if(dt>getTimeForProgramExec()) then
	 *			|	int amountOfExecutions = Integer.parseInt(dt/getTimeForProgramExec())
	 *			|	foreach Ship ship in getShips():
	 *			|		ship.getProgram().getNbOfStatementsExecuted() == amountOfExecutions 
	 *			|			|| ship.getProgram().getNbOfStatementsExecuted == 	getNbOfStatements
	 *	
	 */
	public void evolve(double dt, CollisionListener collisionListener)throws IllegalTimeDurationException{
		if(dt<getTimeForProgramExec()){
			setProgramUnexecutedTime(dt);
		}
		if(getProgramUnexecutedTime()>=getTimeForProgramExec()){
			execAllPrograms();
			resetProgramUnexecutedTime();
		}

		//EIGEN KEUZE VOOR DT ONEINDIG OF 0 OF NEGATIEF OF ISNAN
		double timeToFirstCollision = Double.POSITIVE_INFINITY;
		//long startTime = System.currentTimeMillis();
		if(Double.isNaN(dt) || dt == Double.POSITIVE_INFINITY || dt== Double.NEGATIVE_INFINITY)
			throw new IllegalTimeDurationException("Invalid timeduration dt");
		CollisionObject firstCollisionObject 		= null;
		CollisionObject firstCollisionObjectPartner	= null;

		if(!Util.fuzzyLessThanOrEqualTo(dt, 0)){			//else do nothing (if dt <=0)
			//STEP1: FIRST COLLISION
			CollisionObject[] first= getFirstCollision();
			firstCollisionObject = first[0];
			firstCollisionObjectPartner = first[1];
			if(firstCollisionObject!= null && firstCollisionObjectPartner != null )
				timeToFirstCollision = firstCollisionObject.getTimeToCollision(firstCollisionObjectPartner);


			//0.2 is the smallest
			if(getTimeForProgramExec() < timeToFirstCollision && getTimeForProgramExec() < dt){
				execAllPrograms();
				evolve(dt-getTimeForProgramExec(),collisionListener);
			}

			//STEP2: ADVANCE ALL SHIPS BY TIMETOFIRSTCOLLISION OR GO TO STEP 5
			if(!Util.fuzzyEquals(timeToFirstCollision, dt) && Util.fuzzyLessThanOrEqualTo(timeToFirstCollision, dt)){//timeToFirstCollision < dt)
				for(CollisionObject collisionObject: getGameObjects()){
					collisionObject.move(timeToFirstCollision);
				}

				if(getProgramUnexecutedTime()>=getTimeForProgramExec()){
					execAllPrograms();
					resetProgramUnexecutedTime();
				}

				//STEP3: RESOLVE COLLISION 
				collisionListener.objectCollision(firstCollisionObject, firstCollisionObjectPartner,firstCollisionObject.getXCoordinate(), firstCollisionObject.getYCoordinate());
				firstCollisionObject.collisionReactionWith(firstCollisionObjectPartner);


				//STEP4: SUBSTRACT TIMETOFIRSTCOLLSION AND GO TO STEP1
				evolve(dt-timeToFirstCollision, collisionListener);
			}


			else{//STEP 5

				double evolveTime= dt- getTimeForProgramExec();

				if(!Double.isNaN(evolveTime) && evolveTime != Double.POSITIVE_INFINITY && evolveTime!= Double.NEGATIVE_INFINITY && !Util.fuzzyLessThanOrEqualTo(evolveTime, 0.0)){
					moveGameObjects(getTimeForProgramExec());
					execAllPrograms();
					evolve(dt-getTimeForProgramExec(),collisionListener);
				}
				else{
					moveGameObjects(dt);
				}
			}
			dt = 0;
		}
		else
			throw new IllegalTimeDurationException("That is an illegal amount of time");
	}



	/**
	 * @effect	...
	 * 			| foreach Ship ship in getShips():
	 * 			|	if(ship.getProgram!=null && !ship.getProgram().isFinished()) then
	 * 			|		ship.getProgram().execute();
	 */
	public void execAllPrograms(){
		for(Ship ship: getShips()){
			Program shipProgram = ship.getProgram();
			if(shipProgram!=null && !shipProgram.isFinished()){
				System.out.println("##########  Dit is het execdeel   #######");
				shipProgram.execute();
			}
		}

	}




	/**
	 * 
	 * @param dt
	 * @post	...
	 * 			| new.getProgramUnexecutedTime() == getProgramUnexecutedTime()+dt;
	 */
	public void setProgramUnexecutedTime(double dt){
		programUnexecutedTime+= dt;
	}

	@Basic
	public double getProgramUnexecutedTime(){
		return this.programUnexecutedTime;
	}

	@Basic
	public double getTimeForProgramExec(){
		return timeForProgramExec;
	}

	/**
	 * @post	...
	 * 			|new.getProgramUnExecutedTime() == 0.0;
	 */
	public void resetProgramUnexecutedTime(){
		this.programUnexecutedTime = 0.0;
	}
	private final double timeForProgramExec = 0.2;
	private double programUnexecutedTime;




	/**
	 * @effect	...
	 * 			| for each collisionobject in this.getGameObjects():
	 * 			|		collisionobject.move(dt);
	 * 			|		if(collisionObject.isShip()){
	 *			|			if( ((Ship) collisionObject).isShipThrusterActive()){
	 *			|				((Ship) collisionObject).thrust(dt);}}}
	 */
	private void moveGameObjects(double dt){
		for(CollisionObject collisionObject: getGameObjects()){
			collisionObject.move(dt);
			if(collisionObject.isShip()){
				if( ((Ship) collisionObject).isShipThrusterActive()){

					((Ship) collisionObject).thrust(dt);

				}
			}
		}
	}



	/**
	 * 
	 * @return 	...
	 * 			|"A world with the following specifications: " +
	 *			|"\n\t Height: " + getWorldHeight()+
	 *			|"\n\t Width: " +getWorldWidth()+
	 *			|"\n\t Amount of objects: " + getAmountOfGameObjects()+
	 *			|"\n\n\n\n";
	 */
	@Override
	public String toString(){
		return "A world with the following specifications: " +
				"\n\t Height: " + getWorldHeight()+
				"\n\t Width: " +getWorldWidth()+
				"\n\t Amount of objects: " + getNbOfGameObjects()+
				"\n\n\n\n";
	}

}